<?php

namespace Vipkwd\Utils\Libs\Json;

use Vipkwd\Utils\Crypt;
use Vipkwd\Utils\System\File;

/**
 * @package JsonDb
 * @author  易航 vipkwd
 * @version 2.30 
 * @version 3.0 
 * @link    https://gitee.com/yh_IT/json-db
 *
 */
trait JsonDb
{

    /** 自定义配置项 */
    public $options = [
        'table_name' => null, // 单表模式
        'encode' => false, // 加密函数
        'decode' => true, // 解密函数
        'file_suffix' => '.json', // 文件后缀名
        'path' => null, // 自定义存储路径
        'debug' => true, // 调试模式

    ];

    /** 错误信息 */
    public $error;

    /** JSON数据存储文件夹的根目录 */
    public $tableRoot;

    /** JSON数据表的文件路径 */
    public $tableFile;

    /** 筛选后的结果 */
    public $filterResult = null;

    /** 对数据限制的处理条数 */
    public $limit = null;
    private $_deletionKey = '$_deletion';
    private $_likeNodeKey = '$_search';
    private $defaultCryptKey = '7G2c023l3kCsE083';
    private $pk = 'id';
    private $autoIncrementIndex = 1;
    private $_deletionInclude = false;

    // 聚合列字段
    private $aggregationField = null;
    /**
     * 初始化配置
     * @param array $options JsonDb配置
     * @throws \Exception
     */
    private function __construct($options = null)
    {
        // 更新配置数据
        $this->options = $options ? array_merge($this->options, $options) : $this->options;

        if (empty($this->options['path']) || !is_dir($this->options['path']))
            $this->DbError('请配置数据表的存储目录`path`');

        // 数据存储的目录
        $this->tableRoot = $this->options['path'];

        $this->tableRoot = str_replace(['//', '\\\\'], ['/', '\\'], $this->tableRoot);

        // 单表模式
        $this->options['table_name'] ? $this->table($this->options['table_name']) : false;
    }

    /**
     * 已逻辑删除的数据载入查询范围
     * @return self
     */
    public function deletionInclude()
    {
        $this->_deletionInclude = true;
        return $this;
    }

    /**
     * 插入记录并获取自增ID
     * @access public
     * @param array $data 数据
     * @return integer|string
     */
    public function insert(array $data)
    {
        return $this->_insert($data, true);
    }

    /**
     * 批量添加数据
     * @access public
     * @param array $array 数据集
     * @return integer 返回共添加数据的条数
     */
    public function insertAll(array $array)
    {
        $totals = 0;
        foreach ($array as $value) {
            $totals++;
            if ($totals === $this->limit) {
                break;
            }
            $this->_insert($value);
        }
        return $totals;
    }

    /**
     * 更新或添加
     * 主键匹配则更新
     * @param array $array
     * @param string|null $primary_key 默认自动识别
     * @return int|string
     */
    public function save(array $array, string $primary_key = null)
    {
        if (!$primary_key) {
            $primary_key = $this->pk;
        }
        // 检查要保存的数据中是否存在主键数据
        if (isset($array[$primary_key])) {
            $value = $array[$primary_key];
            // 查询数据表中数据是否存在
            $find = $this->where($primary_key, $value)->find();
            if ($find) {
                unset($array[$primary_key]);
                return $this->where($primary_key, $value)->update($array);
            }
        }
        return $this->_insert($array, true);
    }

    /**
     * 更新记录
     * @access public
     * @param array $array 要更新的数据
     * @return integer 返回更新的键值数量
     */
    public function update(array $array)
    {
        $file = $this->jsonFile();
        $update = 0;
        if (empty($this->filterResult)) {
            return 0;
        }
        $where = $this->filterResult;
        foreach ($where as $key => $value) {
            foreach ($array as $array_key => $array_value) {
                // 不允许更新主键字段
                if (!in_array($array_key, [$this->pk, $this->_deletionKey])) {
                    $update++;
                    $file[$key][$array_key] = $array_value;
                    if (!isset($array['update_time'])) {
                        $file[$key]['update_time'] = date('Y-m-d H:i:s');
                    }
                    if ($update == $this->limit) {
                        break;
                    }
                }
            }
        }
        if ($update) {
            $this->arrayFile($file);
        }
        $this->flushFilter();
        unset($file, $where);
        return $update;
    }

    /**
     * 逻辑删除记录或物理删除字段
     * @access public
     * @param array|null $data 要删除的数据数组字段名，不传值则删除整列数据
     * @return integer  返回影响数据的键值数量
     */
    public function delete(array $data = null)
    {
        if (is_array($data)) {
            return $this->deleteField($data);
        } else {
            return $this->deleteAll();
        }
    }

    /**
     * 物理删除记录
     * @return integer
     */
    public function deletion()
    {
        return $this->deleteAll(true);
    }

    /**
     * 删除数据表
     * @param bool $confirm
     * @return bool
     */
    public function drop(bool $confirm = false)
    {
        if ($confirm === true && file_exists($this->tableFile)) {
            return unlink($this->tableFile);
        }
        return false;
    }

    /**
     * 查询单条数据(支持主键ID查询)
     * @access public
     * @param integer $id 默认执行where过滤
     * @return array|null
     */
    public function find(int $id = null)
    {
        if ($id && $id > 0) {
            $this->where($this->pk, $id);
        }
        $data = $this->_getPrevResult();
        $this->flushFilter();
        if (empty($data)) {
            return null;
        }
        return current($data);
    }

    /**
     * 查询一个字段
     * @access public
     * @param string $field
     * @return null|mixed
     */
    public function value(string $field)
    {
        $find = $this->find();
        if (isset($find[$field])) {
            return $find[$field];
        }
        return null;
    }

    /**
     * 查询多条数据
     * @access public
     * @param bool $withColumnKey 是否返回键名
     * @return array
     */
    public function select(bool $withColumnKey = false)
    {
        $file = $this->_getPrevResult();
        // dump($file);
        $this->flushFilter();
        if (empty($file)) {
            return [];
        }
        if ($withColumnKey)
            return $file;
        return array_values($file);
    }

    /**
     * 查询数据的长度
     * @access public
     * @param array|null $data
     * @return integer
     */
    public function count(array $data = null)
    {
        return count(empty($data) ? $this->select(true) : $data);
    }

    /**
     * 字段递增
     * @access public
     * @param string $field 字段名
     * @param float $step 增长值
     * @return bool
     */
    public function inc(string $field, float $step = 1)
    {
        return $this->numberStepActive($field, $step > 0 ? $step : abs($step));
    }

    /**
     * 字段递减
     * @access public
     * @param string $field 字段名
     * @param float $step 增长值
     * @return bool
     */
    public function dec(string $field, float $step = 1)
    {
        return $this->numberStepActive($field, $step > 0 ? 0 - $step : $step);
    }

    /**
     * 单列平均值
     * @access public
     * @param string $field
     * @return float
     */
    public function avg(string $field)
    {
        $data = $this->_aggregation($field);
        return floatval(array_sum($data) / count($data));
    }
    /**
     * 单列求合
     * @access public
     * @param string $field
     * @return float
     */
    public function sum(string $field)
    {
        return floatval(array_sum($this->_aggregation($field)));
    }
    /**
     * 单列最大值
     * @access public
     * @param string $field
     * @return float
     */
    public function max(string $field)
    {
        return floatval(max($this->_aggregation($field)));
    }
    /**
     * 单列最小值
     * @access public
     * @param string $field
     * @return float
     */
    public function min(string $field)
    {
        return floatval(min($this->_aggregation($field)));
    }
    /**
     * 获取数据表磁盘占用大小
     * @access public
     * @param bool $format 是否格式化量化单位
     * @return bool|int
     */
    public function size(bool $format = false)
    {
        $size = filesize($this->tableFile);
        if ($format) {
            $size = File::bytesTo($size);
        }
        return $size;
    }

    /**
     * 指定查询数量
     * @access public
     * @throws \Exception
     * @param int $limit 查询数量
     * @param int $offset 起始位置
     * @return self
     */
    public function limit(int $limit, int $offset = 0)
    {
        // $this->limit = $offset;
        $this->limit = $limit;
        $file = $this->_getPrevResult();
        if (empty($file)) {
            $this->filterResult = [];
            unset($file, $limit);
            return $this;
        }
        $file = array_values($file);
        // $limit = !is_null($limit) ? ($offset + $limit) : count($file);
        $limit = $offset + $limit;
        $data = [];
        foreach ($file as $key => $value) {
            if ($key >= $offset && $key < $limit) {
                $data[$key] = $value;
            }
        }
        $this->filterResult = $data;
        unset($file, $data, $limit);
        return $this;
    }

    /**
     * 设置分页
     * 
     * $table->where(id, > ,5):Object
     * $table->page():Array
     * $table->select():Array
     * 
     * @param int $page 当前页码
     * @param int $limit
     * @return int[]
     */
    public function page(int $page = 1, int $limit = 10)
    {
        $_filterResult = $this->select();
        $count = $this->count($_filterResult);
        $pages = intval(ceil($count / $limit));
        if ($page < 1)
            $page = 1;
        if ($page > $pages)
            $page = $pages;
        $this->filterResult = $_filterResult;
        $this->limit($limit, ($page - 1) * $limit);
        return [
            'pages' => $pages,
            'current' => $page,
            'limit' => $limit,
            'totals' => $count,
        ];
    }

    /**
     * 指定当前操作的数据表
     * @access public
     * @throws \Exception
     * @param string $name 表名
     * @param string $pk
     * @return self
     */
    public function table($name, string $pk = 'id')
    {
        if (empty($name)) {
            $this->DbError('表名不能为空');
        }
        $this->pk = $pk;
        $this->tableFile = $this->tableRoot . DIRECTORY_SEPARATOR . $name . $this->options['file_suffix'];
        // $this->tableName = $name;
        return $this;
    }

    /**
     * 根据字段条件过滤数组中的元素
     * 
     * ->where(field, value)
     * ->where(field, operator, value)
     * ->where([
     *       field => value,
     *       [field, operator, value],
     *       [field, value]
     *   ])
     * @access public
     * @param string $field 字段名
     * @param mixed  $operator 操作符 默认为 ==
     * @param mixed  $value 字段值
     * @return self
     */
    public function where($field, $operator = null, $value = null)
    {
        $param = func_num_args();
        if ($param === 1 && is_array($field)) {
            $this->whereArray($field);
        }
        if ($param === 2) {
            $this->whereOperator($field, '=', $operator);
        }
        if ($param === 3) {
            $this->whereOperator($field, $operator, $value);
        }
        return $this;
    }

    /**
     * 自定义迭代器
     * @access public
     * @param callable $fn fn返回值指定为输出的item数据
     * @return self
     */
    public function iteration(callable $fn)
    {
        $file = $this->_getPrevResult();
        foreach ($file as $idx => $item) {
            $file[$idx] = $fn($item);
        }
        $this->filterResult = $file;
        unset($file);
        return $this;
    }

    /**
     * 指定查询字段（支持别名）
     * @access public
     * @param string $fields 逗号分隔的字段列表
     * @return self
     */
    public function field(string $fields)
    {
        $fields = str_replace('|', ',', trim($fields));
        $fields = preg_replace("/(\ +)?,(\ +)?/", ',', $fields);
        $fields = preg_replace("/\ +as\ +/i", '#', $fields);
        $fields = preg_replace("/\ +/", '#', $fields);
        $fields = explode(',', $fields . ',' . $this->_likeNodeKey);
        $file = $this->_getPrevResult();
        $data = [];
        foreach ($file as $idx => $item) {
            foreach ($fields as $field) {
                //有字段别名
                if (strpos($field, '#')) {
                    list($key, $alias) = explode('#', $field);
                    //执行别名修改
                    if (isset($item[$key])) {
                        //保留主键
                        if ($key == $this->pk) {
                            $data[$idx][$key] = $item[$field];
                        }
                        $data[$idx][$alias] = $item[$key];
                    }
                    unset($key, $alias);
                    continue;
                }
                if (isset($item[$field])) {
                    $data[$idx][$field] = $item[$field];
                }
                unset($field);
            }
            unset($item);
        }
        $this->filterResult = $data;
        unset($file, $data, $fields);
        return $this;
    }

    /**
     * ORDER排序
     * @access public
     * @param string $field 字段名
     * @param string $order 排序方式 asc|desc
     * @return self
     */
    public function order(string $field, string $order = 'desc')
    {
        $order_list = [
            'asc' => SORT_ASC,
            'desc' => SORT_DESC
        ];
        $order_this = $order_list[$order] ?? $order;
        $file = $this->_getPrevResult();
        foreach ($file as $key => $value) {
            if (!isset($value[$field])) {
                $file[$key][$field] = ($order == 'desc' ? 0 : 99999999);
            }
        }
        $column = array_column($file, $field);
        array_multisort($column, $order_this, $file);
        $this->filterResult = $file;
        unset($column, $order_this, $file);
        return $this;
    }


    /**
     * 插入记录
     * @access private
     * @param array   $data         数据
     * @param boolean $getLastInsID 返回自增主键
     * @return integer|string
     */
    private function _insert(array $data = [], bool $getLastInsID = false)
    {
        // 获取表中原来的数据
        $file = $this->jsonFile();
        $data[$this->pk] = $this->autoIncrementIndex;

        //预设逻辑删除键
        $data[$this->_deletionKey] = 0;

        if (!isset($data['create_time'])) {
            $data['create_time'] = date('Y-m-d H:i:s');
        }
        array_push($file, $data);
        if ($getLastInsID) {
            $this->arrayFile($file, null, true);
            unset($end, $file);
            return $data[$this->pk];
        }
        unset($data, $end);
        return $this->arrayFile($file, null, true);
    }

    /**
     * 数组转JSON数据
     * @access private
     * @param array $array 要转换的数组
     * @return string
     */
    private function jsonEncode(array $array)
    {
        return json_encode($array, empty($this->options['encode']) ? (JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) : JSON_UNESCAPED_UNICODE);
    }
    /**
     * 将数组数据存储到JSON数据表中
     * @access private
     * @param array $array 要存储的数组数据
     * @param string $table_name 自定义表名
     * @return int|false 成功则返回存储数据的总字节，失败则返回false
     */
    private function arrayFile(array $array, $table_name = null, bool $action = false)
    {
        $autoIncrementIndex = $this->autoIncrementIndex;
        if ($action === true) {
            $end = end($array);
            $autoIncrementIndex = $end[$this->pk] + 1;
            unset($end);
        }
        if ($table_name) {
            $this->table($table_name);
        }
        if (!file_exists($this->tableRoot)) {
            mkdir($this->tableRoot, 0755, true);
        }
        $jdb = $this->jsonEncode([
            'deletionKey' => $this->_deletionKey,
            'pk' => $this->pk,
            'autoIncrementIndex' => $autoIncrementIndex,
            'data' => array_values($array)
        ]);
        if ($this->options['encode'] && $this->options['decode']) {
            $jdb = (is_callable($this->options['encode'])) ? call_user_func($this->options['encode'], $jdb) : Crypt::rc4Encrypt($jdb, $this->defaultCryptKey);
        }
        return file_put_contents($this->tableFile, $jdb);
    }
    /**
     * 获取JSON格式的数据表
     * @access private
     * @throws \Exception
     * @param string $option 默认为空 值为id时返回包括ID的数组数据
     * @return array|false
     */
    private function jsonFile()
    {
        if (!file_exists($this->tableFile)) {
            $this->autoIncrementIndex = 1;
            return [];
        }
        $jdb = file_get_contents($this->tableFile);
        if (!self::isJsonText($jdb)) {
            if ($this->options['decode']) {
                $jdb = (is_callable($this->options['decode'])) ? call_user_func($this->options['decode'], $jdb) : Crypt::rc4Decrypt($jdb, $this->defaultCryptKey);
            }
        }
        if (is_string($jdb)) {
            $jdb = json_decode($jdb, true);
        }

        if (!is_array($jdb)) {
            $this->DbError('文件' . $this->tableFile . '数据错误！');
        }
        if (empty($jdb['data'])) {
            $this->autoIncrementIndex = 1;
            return [];
        }
        $this->autoIncrementIndex = $jdb['autoIncrementIndex'];
        return $jdb['data'];
    }

    /**
     * 删除部分字段
     * @access private
     * @param array $array 要删除的部分数据字段名
     * @return integer  返回影响数据的键值数量
     */
    private function deleteField(array $array)
    {
        $file = $this->jsonFile();
        $delete = 0;
        $filter = is_null($this->filterResult) ? [] : $this->filterResult;
        foreach ($filter as $key => $value) {
            foreach ($array as $field) {
                //不允许删除主键字段
                if ($field != $this->pk) {
                    $delete++;
                    unset($file[$key][$field]);
                    if ($delete == $this->limit) {
                        break;
                    }
                }
            }
        }
        if ($delete) {
            $this->arrayFile($file);
        }
        $this->flushFilter();
        unset($file, $filter);
        return $delete;
    }

    /**
     * 删除指定条件所有行数据
     * @access private
     * @param bool $realDeletion 是否物理删除 默认逻辑删除
     * @return integer 返回影响数据的条数
     */
    private function deleteAll(bool $realDeletion = false)
    {
        $file = $this->jsonFile();
        $delete = 0;
        $filter = is_null($this->filterResult) ? [] : $this->filterResult;
        foreach ($filter as $key => $value) {
            $delete++;
            $file[$key][$this->_deletionKey] = time();
            if ($realDeletion) {
                unset($file[$key]);
            }
            if ($delete == $this->limit) {
                break;
            }
        }
        if ($delete) {
            $this->arrayFile($file);
        }
        $this->flushFilter();
        unset($filter, $file);
        return $delete;
    }

    private static function isJsonText($data)
    {
        $prefix = preg_replace("/\s/", '', substr($data, 0, 12));
        $suffix = preg_replace("/\s/", '', substr($data, -12));
        return substr($prefix, 0, 2) == '{"' && substr($suffix, -2) == ']}';
    }
    /**
     * 根据字段条件过滤数组中的元素
     * @access public
     * @param string $field 字段名
     * @param mixed  $operator 操作符 默认为 ==
     * @param mixed  $value 字段值
     * @return self
     */
    private function whereOperator($field, $operator, $value)
    {
        $file = $this->_getPrevResult();
        if (!is_array($file)) {
            $this->filterResult = [];
            return $this;
        }
        switch (strtolower(trim($operator))) {
            case '=':
                $fn = function ($item, $field, $value) {
                    return $item[$field] != $value;
                };
                break;
            case '>':
                $fn = function ($item, $field, $value) {
                    return !($item[$field] > $value);
                };
                break;
            case '>=':
                $fn = function ($item, $field, $value) {
                    return !($item[$field] >= $value);
                };
                break;
            case '<':
                $fn = function ($item, $field, $value) {
                    return !($item[$field] < $value);
                };
                break;
            case '<=':
                $fn = function ($item, $field, $value) {
                    return !($item[$field] <= $value);
                };
                break;
            case 'between':
                $fn = function ($item, $field, $value) {
                    return ($item[$field] < min($value)) || ($item[$field] > max($value));
                };
                break;
            case 'in':
                if (!is_array($value)) {
                    $value = preg_replace("/(\ +)?,(\ +)?/", ',', $value);
                    $value = explode(',', $value);
                }
                $fn = function ($item, $field, $value) {
                    return !in_array($item[$field], $value);
                };
                break;
            case "like":
            case "%":
                unset($file);
                return $this->whereLike($field, $value);
            default:
                $file = [];
                break;
        }
        if (!empty($file) && isset($fn)) {
            foreach ($file as $idx => $item) {
                if (
                    !isset($item[$field]) ||
                    $fn($item, $field, $value) ||
                    ($this->_deletionInclude === false && isset($item[$this->_deletionKey]) && $item[$this->_deletionKey] > 0)
                ) {
                    unset($file[$idx]);
                }
                unset($idx, $item);
            }
            unset($fn, $value);
        }
        $this->filterResult = $file;
        unset($file);
        return $this;
    }

    /**
     * where 数组
     * @access private
     * @param array $array
     * @return self
     */
    private function whereArray(array $array)
    {
        foreach ($array as $key => $value) {
            if (is_array($value)) {
                if (isset($value[2])) {
                    $this->where($value[0], $value[1], $value[2]);
                } else {
                    $this->where($value[0], $value[1]);
                }
            } else {
                $this->where($key, $value);
            }
        }
        return $this;
    }

    /**
     * LIKE查询
     * @access public
     * @param string $field 字段名
     * @param mixed $matchValue 字段值
     * @return self
     */
    private function whereLike($field, $matchValue)
    {
        $file = $this->_getPrevResult();
        $pattern = preg_quote($matchValue, '/');
        if (!preg_match('/%.*%/', $pattern)) {
            if (preg_match('/^%/', $pattern)) {
                $pattern .= '$';
            }
            if (preg_match('/%$/', $pattern)) {
                $pattern = '^' . $pattern;
            }
        }
        $pattern = str_replace('%', '.*', $pattern);
        $pattern = '/' . $pattern . '/s';
        foreach ($file as $key => $item) {
            if (
                !isset($item[$field]) || 
                !preg_match($pattern, $item[$field], $matches) ||
                ($this->_deletionInclude === false && isset($item[$this->_deletionKey]) && $item[$this->_deletionKey] > 0)
            ) {
                unset($file[$key]);
                continue;
            }
            $file[$key][$this->_likeNodeKey] = [
                'field' => $field,
                "query" => $matchValue,
                'pattern' => $pattern,
                'matches' => $matches
            ];
            unset($matches, $item);
        }
        $this->filterResult = $file;
        unset($file, $pattern);
        return $this;
    }

    /**
     * 输出一个错误信息
     * @access private
     * @throws \Exception
     * @param string $msg 错误信息
     * @return void
     */
    private function DbError($msg)
    {
        $this->error = $msg;
        if ($this->options['debug']) {
            throw new \Exception('JsonDb Error：' . $msg);
        }
    }
    /**
     * 数值字段步值操作
     * @access private
     * @param string $field
     * @param float $step
     * @return bool
     */
    private function numberStepActive(string $field, float $step = 1)
    {
        //主键不允许操作
        if (!in_array($field, [$this->pk, $this->_deletionKey])) {
            $where = $this->filterResult;
            if (empty($where)) {
                $this->flushFilter();
                return false;
            }
            foreach ($where as $value) {
                if (isset($value[$field]) && is_numeric($value[$field])) {
                    $value[$field] += $step;
                    $this->save($value);
                }
                unset($value);
            }
            unset($where);
        }
        $this->flushFilter();
        return true;
    }

    /**
     * 聚合函数通用方法
     * @access private
     * @param string $field
     * @return array
     */
    private function _aggregation(string $field)
    {
        $data = [0];
        $field = trim($field);
        if ($field && !in_array($field, [$this->pk, $this->_deletionKey])) {
            $this->iteration(function ($item) use ($field, &$data) {
                if (isset($item[$field]) && is_numeric($item[$field])) {
                    $data[] = $item[$field];
                }
                return null;
            });
            $this->flushFilter();
        }
        return $data;
    }

    private function _getPrevResult()
    {
        return is_null($this->filterResult) ? $this->jsonFile() : $this->filterResult;
    }

    private function flushFilter()
    {
        $this->filterResult = null;
        $this->_deletionInclude = false;
        $this->limit = null;
    }
}